作者:Adam Freeman
翻译:陈广
日期:2019-5-6
Entity Framework Core 的基础是它将 .NET 类的实例表示为关系数据库表中的行。当您在类之间创建关系时,Entity Framework Core 通过在数据库中创建相应的关系来响应。在大多数情况下,创建数据关系的过程是直观和自然的,尽管有许多选项和一些高级特性给粗心大意的人带来了隐患。
在本章中,我将演示如何在数据模型中创建类之间的关系,向您展示 Entity Framework Core 如何响应这些更改,并解释如何在 ASP.NET Core MVC 应用程序中使用关系。表 14-1 为本章简述。
表 14-1:数据关系简述
问题 | 回答 |
---|---|
它们是什么? | 关系允许数据库存储数据库中对象之间的关联。 |
它们有何用途? | 关系允许您更自然地使用 .NET 对象给数据建模,然后将它们及其关联存储到其他对象。 |
如何使用它们 | 通过向数据模型类添加属性,然后使用迁移更新数据库来创建关系。在存储数据时,Entity Framework Core 将尝试自动存储相关数据,尽管这并不总是像您预期的那样工作。 |
是否有任何缺陷或限制? | 关系很复杂,因为数据库管理数据关联的方式并不总是反映 .NET 对象的自然行为。需要付出一些努力才能得到所需的行为。 |
有没有其他选择? | 您根本不需要使用关系,只要您愿意,可以只存储单个对象,尽管这将限制您的应用程序使用简单的数据模型。 |
表 14-2:本章摘要
问题 | 解决方案 | 清单 |
---|---|---|
创建一个关系 | 添加一个导航属性,创建并应用迁移 | 1-4,10 |
在查询中包括关联数据 | 使用Include 和ThenInclude 方法 |
5-9,22-29 |
存储或更新关联数据 | 使用 context 类提供的方法 | 12-15 |
删除关联数据 | 使用 context 类提供的方法 | 16 |
创建所需要关系 | 为一个非空类型添加外键属性 | 17-21 |
本章使用在第11章中创建并在此后的章节中修改的 DataApp 项目。为了准备本章,在 DataApp 文件夹中打开一个命令提示符并运行清单 14-1 所示的命令。
提示:如果您不想跟着构建示例项目,可以从本书的源代码存储库下载所有必需的文件,https://github.com/apress/pro-ef-core-2-for-asp.net-core-mvc。
清单 14-1:重置数据库
dotnet ef database drop --force --context EFDatabaseContext
此命令移除用于存储Product
对象的数据库,以确保您在本章示例中获得正确结果。在我演示了创建数据关系的过程之后,应用程序直到本章的后面部分才会有要处理的数据库或数据。
理解数据关系的最佳方法是创建数据关系。首先,我将向应用程序添加一个新的实体类,以表示产品的供应商。在 Models 文件夹中添加一个名为 Supplier.cs 的类文件,并添加清单 14-2 所示的代码。
清单 14-2:Models 文件夹下的 Supplier.cs 文件的内容
namespace DataApp.Models
{
public class Supplier
{
public long Id { get; set; }
public string Name { get; set; }
public string City { get; set; }
public string State { get; set; }
}
}
新的类定义了一个Id
属性,用于在数据库内存储主键,并定义了Name
、City
和State
属性以提供对供应商的基本描述。此刻这个类没什么特别的,它遵循与之前的例子所使用的相同模式。
要创建一个数据关系,请向Product
类中添加清单 14-3 中所示的属性,它使得每个Product
对象都与一个Supplier
对象关联。
清单 14-3:Models 文件夹下的 Product.cs 文件,添加一个属性
namespace DataApp.Models
{
public enum Colors
{
Red, Green, Blue
}
public class Product
{
public long Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public decimal Price { get; set; }
public Colors Color { get; set; }
public bool InStock { get; set; }
public Supplier Supplier { get; set; }
}
}
新的属性,名为Supplier
,它创建了一个关系,使得每个Product
对象都可以与一个Supplier
对象进行关联。这被称为导航和,因为它使得一个对象可以导航至另一个对象。在这种情况下,如果从Product
对象开始,则可以使用导航属性访问关联的Supplier
对象。
命名导航属性
在清单14-3中,我为创建与
Supplier
对象关系的导航属性使用了名称Supplier
。您不必为导航属性使用相关类的名称,尽管这样做通常很方便,就像在本例中那样。您可以为 C# 属性使用任何合法名称,在本章后面的一个示例中,我命名了一个属性Location
,以创建与一个名为ContactLocation
的类的实例的关系。
要了解 Entity Framework Core 如何处理关系,可以考虑Supplier
属性对应用程序中的 .NET 对象的影响,而不必担心数据库。对数据模型的更改意味着每个Product
对象都可以与Supplier
对象相关,如图 14-1 所示。
一个Product
对象的Supplier
属性可以为null
,这意味着Product
对象并非总是与Supplier
关联。一个Product
对象的Supplier
属性并不一定是惟一的,这意味着多个Product
对象可以与相同的Supplier
关联。
要查看 Entity Framework Core 如何处理附加的导航属性,在 DataApp 项目文件夹中运行清单 14-4 所示的命令以创建一个迁移。
清单 14-4:创建一个迁移
dotnet ef migrations add Add_Supplier --context EFDatabaseContext
Entity Framework Core 将创建一个名为 Add_Supplier 的迁移,如果您检查 Migrations 文件夹下的 <timestamp>_Add_Supplier.cs
文件中的Up
方法,可以看到 Entity Framework Core 是如何升级数据库的。Up
方法包含了4条语句用于升级数据库。第一条语句在已存的数据表中添加一个新列。
migrationBuilder.AddColumn<long>(
name: "SupplierId",
table: "Products",
nullable: true);
这是 Entity Framework Core 将使用的列,用于跟踪与Product
对象关联的Supplier
对象。AddColumn
方法的参数将在现有的Products
表上创建一个名为SupplierId
的列。nullable
参数设置为true
,这意味着在新列中允许null
值,以便与Supplier
无关的Product
对象仍然可以存储在数据库中。
迁移的Up
方法中的第二个语句创建了一个新表,该表将用于存储Supplier
对象。
migrationBuilder.CreateTable(
name: "Supplier",
columns: table => new
{
Id = table.Column<long>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
Name = table.Column<string>(nullable: true),
City = table.Column<string>(nullable: true),
State = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Supplier", x => x.Id);
});
CreateTable
方法的参数将创建一个名为Supplier
的表,该表具有Id
、Name
、City
和State
列,这些列对应于Supplier
类定义的属性。Id
列被配置为主键,当向表中添加新行时,数据库服务器将负责为该列生成值。
提示:注意,表的名称是
Supplier
,而现有表的名称是Products
。Entity Framework Core 的约定是在为通过 context 类访问的实体类创建表时使用DbSet<T>
属性的名称,并为只能通过关系访问的实体类使用导航属性的名称。
下一条语句创建了一个索引,用于加速数据库查询:
migrationBuilder.CreateIndex(
name: "IX_Products_SupplierId",
table: "Products",
column: "SupplierId");
索引与表示Product
和Supplier
对象之间的关系没有直接关联,本章我将暂时跳过它。Up
方法中的最后一条语句负责在Product
和Supplier
表中的行之间创建链接:
migrationBuilder.AddForeignKey(
name: "FK_Products_Supplier_SupplierId",
table: "Products",
column: "SupplierId",
principalTable: "Supplier",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
AddForeignKey
方法用于配置添加到Products
表中的SupplierId
列,以创建与新Supplier
表上的Id
列的外键关系。这有助于保护数据库的完整性,方法是确保Product
表中的行只能具有与Supplier
表中的有效行相对应的SupplierId
列的值(或值为null
,指示与Supplier
没有关系)。
迁移的Up
方法中语句的组合效果是更新数据库以反映数据模型中的变化,如图 14-2 所示。
Supplier
表允许 Entity Framework Core 存储Supplier
对象,添加到Products
表中的SupplierId
列将用于跟踪Product
和Supplier
对象之间的关系。
在本章中,我将解释如何对关联数据执行完整的数据操作,但我将从最基本的任务开始,即向用户显示关联数据值。默认情况下, Entity Framework Core 不遵循导航属性来加载相关数据。这是为了防止查询返回不需要的数据。要加载与Product
对象关联的Supplier
数据,请将清单 14-5 所示的语句添加到存储库实现类中。
清单 14-5:Models 文件夹下的 EFDataRepository.cs 文件,查询关联数据
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore;
namespace DataApp.Models
{
public class EFDataRepository : IDataRepository
{
private EFDatabaseContext context;
public EFDataRepository(EFDatabaseContext ctx)
{
context = ctx;
}
public Product GetProduct(long id)
{
return context.Products.Include(p => p.Supplier).First(p => p.Id == id);
}
public IEnumerable<Product> GetAllProducts()
{
Console.WriteLine("GetAllProducts");
return context.Products.Include(p => p.Supplier);
}
public IEnumerable<Product> GetFilteredProducts(string category = null,
decimal? price = null, bool includeRelated = true)
{
IQueryable<Product> data = context.Products;
if (category != null)
{
data = data.Where(p => p.Category == category);
}
if (price != null)
{
data = data.Where(p => p.Price >= price);
}
if (includeRelated)
{
data = data.Include(p => p.Supplier);
}
return data;
}
public void CreateProduct(Product newProduct)
{
newProduct.Id = 0;
context.Products.Add(newProduct);
context.SaveChanges();
Console.WriteLine($"New Key: {newProduct.Id}");
}
public void UpdateProduct(Product changedProduct, Product originalProduct = null)
{
if (originalProduct == null)
{
originalProduct = context.Products.Find(changedProduct.Id);
}
else
{
context.Products.Attach(originalProduct);
}
originalProduct.Name = changedProduct.Name;
originalProduct.Category = changedProduct.Category;
originalProduct.Price = changedProduct.Price;
context.SaveChanges();
}
public void DeleteProduct(long id)
{
context.Products.Remove(new Product { Id = id });
context.SaveChanges();
}
}
}
Include
扩展方法是在Microsoft.EntityFrameworkCore
命名空间定义的,并调用实现了IQueryable<T>
接口的对象来包含关联数据。Include
方法的参数是一个表达式,它选择 Entity Framework Core 应该遵循的导航属性来获取关联数据。在清单中,我选择Supplier
对象。Include
方法可以像其他 LINQ 方法一样合并到查询中,如下所示:
...
return context.Products.Include(p => p.Supplier);
...
并不是所有 LINQ 扩展方法都可以用于Include
方法返回的对象,因此在使用关联数据时可能需要重新处理一些查询。我在前面的示例中使用了Find
方法来定位数据库中的特定Product
,但它不能与Include
一起使用,所以我用First
方法替换了它,它以类似的方式工作,如下所示:
...
return context.Products.Include(p => p.Supplier).First(p => p.Id == id);
...
您可以决定是否在运行时将关联数据包含在查询中。在清单14-5中,我更改了GetFilteredProducts
方法,以便当includeRelated
参数的值为true
时调用Include
方法而不是其它方法,如下所示:
...
if (includeRelated)
{
data = data.Include(p => p.Supplier);
}
...
为了支持有选择地将关联数据包含到应用程序的其他部分,更改IDataRepository
接口的签名,如清单14-6所示。
未来对延迟加载的支持
早期版本的 Entity Framework 支持一种称为*延迟加载(lazy loading)*的功能,微软已经宣布,该功能将包含在 Entity Framework Core 的未来版本中。在延迟加载中,当读取导航属性时,关联数据将自动从数据库加载。对于示例应用程序,这意味着读取
Product
对象上的Supplier
属性的值将自动触发对创建Supplier
对象所需数据的SQL查询。延迟加载听起来是个好主意,而且通常是作为一种方便的功能启用的,它允许编写应用程序的 MVC 部分,而不必创建 Entity Framework Core 查询,这些查询包括或排除针对不同 action 的不同相关数据集。一切都像魔术一样工作,Entity Framework Core 确保应用程序的 MVC 部分所需的数据是可用的。但是在后台,在读取导航属性时会生成额外的 SQL 查询,增加了数据库服务器的负载,增加了处理 HTTP 请求所需的时间。
我的建议是避免延迟装载。相反,定义存储库方法或属性,这些方法或属性以应用程序的 MVC 部分所需的所有组合返回数据,无论是否有关联数据。换句话说,避免任何自动生成数据库查询的特性。
清单 14-6:Models 文件夹下的 IDataRepository.cs 文件,添加一个参数
using System.Collections.Generic;
using System.Linq;
namespace DataApp.Models
{
public interface IDataRepository
{
Product GetProduct(long id);
IEnumerable<Product> GetAllProducts();
IEnumerable<Product> GetFilteredProducts(string category = null,
decimal? price = null, bool includeRelated = true);
IEnumerable<Product> GetFilteredProducts(string category = null,
decimal? price = null);
void CreateProduct(Product newProduct);
void UpdateProduct(Product changedProduct, Product originalProduct = null);
void DeleteProduct(long id);
}
}
接下来,向 action 方法添加一个参数,以便控制器可以通过 MVC 模型绑定过程获得是否包含来自 HTTP 请求的关联数据的值,如清单14-7所示。
清单 14-7:Controllers 文件夹下的 HomeController.cs 文件,添加一个 action 参数
using Microsoft.AspNetCore.Mvc;
using DataApp.Models;
using System.Linq;
namespace DataApp.Controllers
{
public class HomeController : Controller
{
private IDataRepository repository;
public HomeController(IDataRepository repo)
{
repository = repo;
}
public IActionResult Index(string category = null,
decimal? price = null, bool includeRelated = true)
{
var products = repository
.GetFilteredProducts(category, price, includeRelated);
ViewBag.category = category;
ViewBag.price = price;
ViewBag.includeRelated = includeRelated;
return View(products);
}
// ...此处省略...
}
Entity Framework Core 处理关联数据的一个缺点是,没有一种很好的方法来计算导航属性的值是否为null
。这要么是因为没有特定对象的关联数据,要么是因为没有使用Include
方法选择关联数据。为了解决这个问题,我使用了ViewBag
属性,以便视图能够知道关联数据是否已被请求。
现在,所有的代码更改都已经就绪,我可以使用 Razor 显示相关数据。模型数据将包括请求时的Supplier
对象,并且有一个ViewBag
属性可用于配置表单元素,如清单 14-8 所示。
清单 14-8:Views/Home 文件夹下的 Index.cshtml 文件,显示关联数据
@model IEnumerable<DataApp.Models.Product>
@{
ViewData["Title"] = "Products";
Layout = "_Layout";
}
<div class="m-1 p-2">
<form asp-action="Index" method="get" class="form-inline">
<label class="m-1">Category:</label>
<select name="category" class="form-control">
<option value="">All</option>
<option selected="@(ViewBag.category == "Watersports")">
Watersports
</option>
<option selected="@(ViewBag.category == "Soccer")">Soccer</option>
<option selected="@(ViewBag.category == "Chess")">Chess</option>
</select>
<label class="m-1">Min Price:</label>
<input class="form-control" name="price" value="@ViewBag.price" />
<div class="form-check m-1">
<label class="form-check-label">
<input class="form-check-input" type="checkbox"
name="includeRelated" value="true"
checked="@(ViewBag.includeRelated == true)" />
Related Data
</label>
<input type="hidden" name="includeRelated" value="false" />
</div>
<button class="btn btn-primary m-1">Filter</button>
</form>
</div>
<table class="table table-sm table-striped">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Category</th>
<th>Price</th>
@if (ViewBag.includeRelated)
{
<th>Supplier</th>
}
</tr>
</thead>
<tbody>
@foreach (var p in Model)
{
<tr>
<td>@p.Id</td>
<td>@p.Name</td>
<td>@p.Category</td>
<td>$@p.Price.ToString("F2")</td>
@if (ViewBag.includeRelated)
{
<td>@p.Supplier?.Name</td>
}
<td>
<form asp-action="Delete" method="post">
<a asp-action="Edit" class="btn btn-sm btn-warning"
asp-route-id="@p.Id">
Edit
</a>
<input type="hidden" name="id" value="@p.Id" />
<button type="submit" class="btn btn-danger btn-sm">
Delete
</button>
</form>
</td>
</tr>
}
</tbody>
</table>
<a asp-action="Create" class="btn btn-primary">Create New Product</a>
我添加了一个复选框,它将控制请求中是否包含关联数据。当用户选择了关联数据时,ViewBag.includeRelated
属性将为true
,视图将使用此值在表中显示另一列。该列的值是通过读取与正在显示的Product
对象关联的Supplier
对象的Name
属性获得的,如下所示:
...
<td>@p.Supplier?.Name</td>
...
Razor 视图不知道Supplier
对象的来源,因为 Entity Framework Core 使用常规的 .NET 对象填充导航属性。
我将演示如何对关联数据执行完整的数据操作,但如果有一些初始数据来演示如何使用Include
方法进行查询,则会有所帮助。要将Supplier
对象添加到数据库中,请对种子数据类进行清单 14-9 所示的更改。
清单 14-9:Models 文件夹下的 SeedData.cs 文件,给数据库中的供应商播种
...
private static Product[] Products
{
get
{
Product[] products = new Product[]
{
new Product { Name = "Kayak", Category = "Watersports",
Price = 275, Color = Colors.Green, InStock = true },
new Product { Name = "Lifejacket", Category = "Watersports",
Price = 48.95m, Color = Colors.Red, InStock = true },
new Product { Name = "Soccer Ball", Category = "Soccer",
Price = 19.50m, Color = Colors.Blue, InStock = true },
new Product { Name = "Corner Flags", Category = "Soccer",
Price = 34.95m, Color = Colors.Green, InStock = true },
new Product { Name = "Stadium", Category = "Soccer",
Price = 79500, Color = Colors.Red, InStock = true },
new Product { Name = "Thinking Cap", Category = "Chess",
Price = 16, Color = Colors.Blue, InStock = true },
new Product { Name = "Unsteady Chair", Category = "Chess",
Price = 29.95m, Color = Colors.Green, InStock = true },
new Product { Name = "Human Chess Board", Category = "Chess",
Price = 75, Color = Colors.Red, InStock = true },
new Product { Name = "Bling-Bling King", Category = "Chess",
Price = 1200, Color = Colors.Blue, InStock = true }
};
Supplier s1 = new Supplier
{
Name = "Surf Dudes",
City = "San Jose",
State = "CA"
};
Supplier s2 = new Supplier
{
Name = "Chess Kings",
City = "Seattle",
State = "WA"
};
products.First().Supplier = s1;
foreach (Product p in products.Where(p => p.Category == "Chess"))
{
p.Supplier = s2;
}
return products;
}
}
...
我已经用一个属性替换了SeedData
类中的Product
对象的静态数组,该属性的 get 访问器创建了两个Supplier
对象,并将它们与Product
对象相关联。Surf Dudes
供应商与第一个Product
对象Kayak
相关联,而Chess Kings
供应商与Chess
类别中的所有Product
对象相关联。
接下来,通过应用迁移为应用程序准备数据库,包括定义Supplier
关系的新迁移。要应用迁移,请在 DataApp 项目文件夹中运行清单 14-10 所示的命令。
清单 14-10:准备数据库
dotnet ef database update --context EFDatabaseContext
要启动应用程序,打开一个命令提示符,导航至 DataApp 文件夹,通过运行清单 14-11 中的命令启动应用程序。
清单 14-11:启动示例应用程序
dotnet run
应用程序将使用Product
和Supplier
对象播种数据库,需要花一些时间才能完成。一旦应用程序启动,打开浏览器并导航至 http://localhost:5000。您将在表中看到包含与Supplier
对象关联的Product
对象的供应商名称的新列,如图14-3所示。请注意,并非所有的Product
对象都与Supplier
关联,而且,在这些对象中,不止一个与同一供应商有关系。
如果检查应用程序的控制台输出,您将看到 Entity Framework Core 用于获取关联数据的查询。
...
SELECT [p].[Id], [p].[Category], [p].[Color], [p].[InStock], [p].[Name],
[p].[Price], [p].[SupplierId], [p.Supplier].[Id], [p.Supplier].[City],
[p.Supplier].[Name], [p.Supplier].[State]
FROM [Products] AS [p]
LEFT JOIN [Supplier] AS [p.Supplier] ON [p].[SupplierId] = [p.Supplier].[Id]
...
查询使用 Products 和 Supplier 表之间的关系来执行连接,以便可以在单个查询中获得所需的所有数据。如果取消选中【Related Data】选项并单击【Filter】按钮,则对数据库的查询不会请求 Supplier 数据,Supplier 列将不会在浏览器显示的表中。
创建或更新关联数据的过程通过导航属性执行,并使用标准 .NET 对象完成。
对于示例应用程序——与大多数实际项目一样——有三种不同的场景,其中一个Supplier
对象存储在数据库中,或者一个现有的Supplier
对象被更新。
Product
和Supplier
对象同时被创建。Supplier
对象被创建并与一个现有的Product
对象关联。Supplier
对象在与Product
对象关联后被修改。为了准备这些操作,我在 Views/Shared 文件夹中添加了一个名为 Supplier.cshtml 的视图,内容如清单 14-12 所示。
清单 14-12:Views/Shared 文件夹下的 Supplier.cshtml 文件的内容
@model DataApp.Models.Product
<input type="hidden" asp-for="Supplier.Id" />
<div class="form-group">
<label asp-for="Supplier.Name"></label>
<input asp-for="Supplier.Name" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Supplier.City"></label>
<input asp-for="Supplier.City" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Supplier.State"></label>
<input asp-for="Supplier.State" class="form-control" />
</div>
此视图的模型是一个Product
对象。在一个用于处理Supplier
数据的视图中,这可能看起来很奇怪,但遵循 MVC 约定并使用asp-for
标签助手设置 HTML 元素,这样 MVC 模型绑定过程将使用来自input
元素的值创建一个Supplier
对象,并将其分配给它创建的Product
对象的Supplier
属性。
编辑 Views/Home 文件夹下的 Editor.cshtml 视图,如清单 14-13 所示,混合清单 14-12 中的视图,并向视图内容中添加一些结构。
清单 14-13:Views/Home 文件夹下的 Editor.cshtml 文件,使用分部视图
@model DataApp.Models.Product
@{
ViewData["Title"] = ViewBag.CreateMode ? "Create" : "Edit";
Layout = "_Layout";
}
<form asp-action="@(ViewBag.CreateMode ? "Create" : "Edit")" method="post">
<input name="original.Id" value="@Model?.Id" type="hidden" />
<input name="original.Name" value="@Model?.Name" type="hidden" />
<input name="original.Category" value="@Model?.Category" type="hidden" />
<input name="original.Price" value="@Model?.Price" type="hidden" />
<div class="row m-1">
<div class="col-6">
<h5 class="bg-info text-center p-2 text-white">Product</h5>
<div class="form-group">
<label asp-for="Name"></label>
<input asp-for="Name" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Category"></label>
<input asp-for="Category" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Price"></label>
<input asp-for="Price" class="form-control" />
</div>
</div>
<div class="col-6">
<h5 class="bg-info text-center p-2 text-white">Supplier</h5>
@await Html.PartialAsync("Supplier", Model)
</div>
</div>
<div class="text-center">
<button class="btn btn-primary" type="submit">Save</button>
<a asp-action="Index" class="btn btn-secondary">Cancel</a>
</div>
</form>
应用于清单14-13中div
元素的 Bootstrap 样式创建了一个简单的网格,以便表单元素显示在两列中,一列用于Product
类定义的字段,另一列用于Supplier
类。您将在下一节中看到创建的布局。
第一个场景是最简单的:同时将新Product
和Supplier
对象添加到数据库中。这不需要对应用程序做进一步的更改,因为创建新Product
对象的现有代码将自动处理Supplier
对象。
启动应用程序,使用浏览器导航到 http://localhost:5000,并单击【Create New Product】按钮。
填写表单中的两列,如图 14-4 所示,并单击【Save】按钮。如果要重新创建图中所示的产品和供应商,请使用表14-3中所示的值。
表 14-3:图中使用的产品和供应商详细信息
字段 | 值 |
---|---|
Product Name | Running Shoes |
Category | Running |
Price | 100 |
Supplier Name | Zoom Shoes |
City | San Jose |
State | CA |
单击【Save】按钮时,浏览器向服务器发送一个 HTTP POST 请求,该请求包含清单14-12中定义的input
元素的值。这些input
元素是由 Razor 渲染的,因此 MVC 模型绑定器会将它们识别为Supplier
对象的属性。例如,以下是City
属性的元素如何渲染的:
...
<input class="form-control" type="text"
id="Supplier_City" name="Supplier.City" value="">
...
模型绑定器使用 HTML 表单中的值来创建Product
对象和Supplier
对象,并将其分配给Product
对象的Supplier
导航属性。Product
对象用作 Home 控制器的Create
方法的参数,后者将其传递给存储库的CreateProduct
方法。存储库的CreateProduct
方法将Product
对象添加到由 Context 对象管理的集合中,并调用SaveChanges
方法。Entity Framework Core 检查Product
对象,发现Id
值为 0,这表明它是要存储在数据库中的新对象。Entity Framework Core 还遵循Product.Supplier
导航属性来检查Supplier
对象,并看到它的Id
值也是零,并且是一个要存储在数据库中的新对象。这些对象作为新行存储在Products
和Supplier
表中,并将Products
行上的SupplierId
列的值设置为Supplier
行上的Id
列的值,以记录对象之间的关系。
如果检查应用程序生成的日志记录消息,将看到存储数据的两组 SQL 语句。第一对语句在 Supplier 表中创建行,该表存储Supplier
对象并获取数据库服务器分配的主键值。
...
INSERT INTO [Supplier] ([City], [Name], [State])
VALUES (@p0, @p1, @p2);
SELECT [Id]
FROM [Supplier]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
...
第二对语句在 Products 表中创建行,并获取数据库服务器分配的主键。
...
INSERT INTO [Products] ([Category], [Color], [InStock],
[Name], [Price], [SupplierId])
VALUES (@p3, @p4, @p5, @p6, @p7, @p8);
SELECT [Id]
FROM [Products]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
...
结果是,Entity Framework Core 与 MVC 模型绑定器创建的对象无缝地工作,并在数据库中存储两个新行来表示新对象及其之间的关系,如图14-5所示。
当与其相关的Product
对象以前存储在数据库中时,需要对 context 类进行更改,以创建或更新Supplier
对象。这是因为我在前面使用的技术利用了 Entity Framework Core 提供的更改检测功能,以减少执行的数据库查询的数量,并且必须扩展这些查询以包括Supplier
数据。要将Supplier
数据包含在发送给客户端的 HTML 表单中,请将清单 14-14 所示的元素添加到 Supplier.cshtml 视图中。
清单 14-14:Views/Shared 文件夹下的 Supplier.cshtml 文件,添加现在值
@model DataApp.Models.Product
<input name="original.Supplier.Id" value="@Model.Supplier?.Id" type="hidden" />
<input name="original.Supplier.Name" value="@Model.Supplier?.Name" type="hidden" />
<input name="original.Supplier.City" value="@Model.Supplier?.City" type="hidden" />
<input name="original.Supplier.State" value="@Model.Supplier?.State" type="hidden" />
<input type="hidden" asp-for="Supplier.Id" />
<div class="form-group">
<label asp-for="Supplier.Name"></label>
<input asp-for="Supplier.Name" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Supplier.City"></label>
<input asp-for="Supplier.City" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Supplier.State"></label>
<input asp-for="Supplier.State" class="form-control" />
</div>
这些隐藏的input
元素与 Editor.cshtml 视图中的元素对应。要确保将用户所做的更改写入数据库,请编辑EFDataRepository
类的UpdateProduct
方法,并添加清单 14-15 所示的语句。
清单 14-15:Models 文件夹下的 EFDataRepository.cs 文件,包含供应商数据
public void UpdateProduct(Product changedProduct,
Product originalProduct = null)
{
if (originalProduct == null)
{
originalProduct = context.Products.Find(changedProduct.Id);
}
else
{
context.Products.Attach(originalProduct);
}
originalProduct.Name = changedProduct.Name;
originalProduct.Category = changedProduct.Category;
originalProduct.Price = changedProduct.Price;
originalProduct.Supplier.Name = changedProduct.Supplier.Name;
originalProduct.Supplier.City = changedProduct.Supplier.City;
originalProduct.Supplier.State = changedProduct.Supplier.State;
context.SaveChanges();
}
这些语句将Name
、City
和State
值复制到由 Entity Framework Core 跟踪的对象,以确保更改的值存储在数据库中。请注意,我没有复制Supplier.Id
属性;该值由 Entity Framework Core 或数据库服务器管理,最好在执行更新时保持独处。
要测试更改,请重新启动应用程序,导航到 http://localhost:5000,然后单击“Thinking Cap”产品的【Edit】按钮。要查看更新两个关联对象的效果,请进行表 14-4 中所示的更改。
表 14-4:编辑 Thinking Cap 产品和供应商的值
字段 | 值 |
---|---|
Product Name | Thinking Cap (Medium) |
Supplier Name | The Pawn Brokers |
City | Chicago |
State | IL |
单击【Save】按钮,浏览器将向应用程序发送更改的数据。Product
和Supplier
对象将由模型构建器根据请求数据创建,Entity Framework Core 将保存对数据库的更改。由于Supplier
对象与Chess
类别中的所有Product
对象关联,所以更改将反映在表中的几行中,如图 14-6 所示。
默认情况下,Entity Framework Core 不会遵循导航属性从数据库中删除相关数据。这意味着删除Product
对象并不会删除与其关联的Supplier
对象。
这是因为Supplier
对象可能与其他Product
对象关联,将其从数据库中删除将使数据不一致,这是数据库服务器难以避免的。有些特性使执行删除操作更容易,我在后面的章节中描述了这些特性,但目前,我将演示默认配置,并解释它所带来的缺陷。
要查看删除操作是如何处理的,请启动应用程序,导航到 http://localhost:5000,并单击与“The Pawn Brokers”产品相关的不“Unsteady Chair”产品的【Delete】按钮。此操作移除产品,但保留Supplier
对象,该对象仍然与“Chess”类别中的其他产品相关,如图 14-7 所示。
您可以告诉 Entity Framework Core 删除关联数据,但如果这样做会使数据库不一致,则数据库服务器将导致异常。要了解这是如何工作的,请对 EFDataRepository 类中的DeleteProduct
方法进行清单 14-16 所示的更改。
清单 14-16:Models 文件夹下的 EFDataRepository.cs 文件,删除关联数据
public void DeleteProduct(long id)
{
Product p = this.GetProduct(id);
context.Products.Remove(p);
if (p.Supplier != null)
{
context.Remove<Supplier>(p.Supplier);
}
context.SaveChanges();
}
清单 14-16 中的语句调用GetProduct
方法,该方法从数据库检索产品及其相关供应商。Product
对象通过 context 对象的DbSet<Product>
属性提供的Remove
方法删除。无法直接访问Supplier
数据,但 context 对象的Remove<T>
方法可用于从数据库中删除任何对象,如下所示:
...
context.Remove<Supplier>(p.Supplier);
...
要测试这段代码,请重新启动应用程序,导航到 http://localhost:5000,然后单击“Running Shoes”产品的【Delete】按钮,如图 14-8 所示。这是唯一与“Zoom Shoes”供应商对象关联的Product
对象,这意味着删除这些对象不会造成任何不一致,两者都将被删除。
若要查看数据库服务器如何防止不一致,请单击“Human ChessBoard”产品的【Delete】按钮。由于此产品的相关供应商也与其他产品对象相关,因此删除它将使 Products 表中的行在其SupplierId
列中保留值,这些值指向 Supplier 表中不存在的行。图 14-9 中的异常显示了数据库服务器如何不会执行会导致此问题的操作。
从目前的情况来看,删除操作并不特别有用,但我将在本章后面再讨论这个主题,并解释如何使用不同类型的关系可以使删除操作更有用和更可预测。
Product
和Supplier
类有一个可选的关系,这反映了一个Product
不必与Supplier
关联的事实。这是关系的默认类型,反映了对象在应用程序的 MVC 部分中的工作方式,其中引用另一个对象的属性可能为null
。
有些关系需要更加正式,其中一种类型的对象总是与另一种类型的对象关联是很重要的。在这些情况下,您可以创建一个必要关系,它重新配置数据库以代表应用程序强制执行关系。
创建必要关系意味着告诉 Entity Framework Core 应该如何创建跟踪数据库中关系的外键列。以下是创建 Products 和 Supplier 表之间关系的迁移语句:
...
migrationBuilder.AddColumn<long>(name: "SupplierId",
table: "Products", nullable: true);
...
migrationBuilder.AddForeignKey(name: "FK_Products_Supplier_SupplierId",
table: "Products", column: "SupplierId", principalTable: "Supplier",
principalColumn: "Id", onDelete: ReferentialAction.Restrict);
...
Entity Framework Core 没有提供任何有关外键关系的信息,并且使用了一些合理的默认值,包括允许在跟踪关系的列中使用null
值,以及创建一个外键关系,其命名组合了所涉及的表和列的名称。
覆盖默认值的方法是创建一个属性,该属性向 Entity Framework Core 提供有关如何创建外键的详细信息。这是一个外键属性,它在包含导航属性的同一个类中定义。在清单 14-17 中,我在Product
类中定义了一个外键属性,该属性将关系配置为Supplier
对象。
清单 14-17:Models 文件夹下的 Product.cs 文件,添加外建属性
namespace DataApp.Models
{
public enum Colors
{
Red, Green, Blue
}
public class Product
{
public long Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public decimal Price { get; set; }
public Colors Color { get; set; }
public bool InStock { get; set; }
public long SupplierId { get; set; }
public Supplier Supplier { get; set; }
}
}
该属性的名称通过将导航属性名称或关联的类名与主键的名称结合起来,告诉 Entity Framework Core 它所关联的导航属性。对于Product
类,这两个约定都会导致一个名为SupplierId
的属性,Entity Framework Core 将其检测为Supplier
导航属性的外键属性。(在第18章中,我将向您展示如何重写属性名称的约定。)
外键属性的类型告诉 Entity Framework Core,关系是可选的还是必要的。如果可以将外键属性类型设置为null
,则关系将是可选的(例如,类型为Long?
时);如果不能使用空值(例如类型为Long
时),则为必要。在清单 14-17 中,我使用了long
作为属性类型;这告诉 Entity Framework Core 创建必要关系,因为long
值不能设置为null
。
在 DataApp 项目文件夹下运行清单 14-18 所示的命令,创建一个更改数据库以应用属性的迁移。
清单 14-18:创建迁移
dotnet ef migrations add Required --context EFDatabaseContext
如果您检查 Migrations 文件夹下的<timestamp>_Required.cs
文件的Up
方法中的语句,将看到应用到数据库的更改,如下:
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Products_Supplier_SupplierId",
table: "Products");
migrationBuilder.AlterColumn<long>(
name: "SupplierId",
table: "Products",
nullable: false,
oldClrType: typeof(long),
oldNullable: true);
migrationBuilder.AddForeignKey(
name: "FK_Products_Supplier_SupplierId",
table: "Products",
column: "SupplierId",
principalTable: "Supplier",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
迁移将重新创建Products
和Supplier
表之间的外键关系,以反映Product
类上的新属性。最重要的语句是更改SupplierId
列的语句,它的nullable
参数是用来防止空值的。
migrationBuilder.AlterColumn<long>(
name: "SupplierId",
table: "Products",
nullable: false,
oldClrType: typeof(long),
oldNullable: true);
更改后的列不再允许存储null
值,外键要求值对应于Supplier
表中的一行,这是管理所需关系的方式。
如果试图将迁移应用到数据库,将收到一个错误,因为Products
表中存在包含NULL
值的行,而这已经不再允许。
在实际项目中,您应该通过删除不符合新要求的数据来为迁移准备数据库。对于示例应用程序,我将完全删除数据库,更新种子数据,然后重新创建它,以确保所有数据都是有效的。首先,在 DataApp 项目文件夹中运行清单 14-19 所示的命令以删除数据库。
清单 14-19:删除数据库
dotnet ef database drop --force --context EFDatabaseContext
接下来,更新由SeedData
类定义的Products
属性的 get 访问器,以便存储的所有Product
对象都与一个Supplier
对象关联,如清单 14-20 所示。
清单 14-20:Models 文件夹下的 SeedData.cs 文件,确保数据关系
...
private static Product[] Products
{
get
{
Product[] products = new Product[]
{
new Product { Name = "Kayak", Category = "Watersports",
Price = 275, Color = Colors.Green, InStock = true },
new Product { Name = "Lifejacket", Category = "Watersports",
Price = 48.95m, Color = Colors.Red, InStock = true },
new Product { Name = "Soccer Ball", Category = "Soccer",
Price = 19.50m, Color = Colors.Blue, InStock = true },
new Product { Name = "Corner Flags", Category = "Soccer",
Price = 34.95m, Color = Colors.Green, InStock = true },
new Product { Name = "Stadium", Category = "Soccer",
Price = 79500, Color = Colors.Red, InStock = true },
new Product { Name = "Thinking Cap", Category = "Chess",
Price = 16, Color = Colors.Blue, InStock = true },
new Product { Name = "Unsteady Chair", Category = "Chess",
Price = 29.95m, Color = Colors.Green, InStock = true },
new Product { Name = "Human Chess Board", Category = "Chess",
Price = 75, Color = Colors.Red, InStock = true },
new Product { Name = "Bling-Bling King", Category = "Chess",
Price = 1200, Color = Colors.Blue, InStock = true }
};
Supplier acme = new Supplier
{
Name = "Acme Co",
City = "New York",
State = "NY"
};
Supplier s1 = new Supplier
{
Name = "Surf Dudes",
City = "San Jose",
State = "CA"
};
Supplier s2 = new Supplier
{
Name = "Chess Kings",
City = "Seattle",
State = "WA"
};
products.First().Supplier = s1;
foreach (Product p in products)
{
if (p == products[0])
{
p.Supplier = s1;
}
else if (p.Category == "Chess")
{
p.Supplier = s2;
}
else
{
p.Supplier = acme;
}
}
return products;
}
}
...
对种子数据的更改保留了我以前建立的关系,并将Acme Co
供应商用于所有其他产品。
在 DataApp 项目文件夹中运行清单 14-21 所示的命令,以准备数据库,包括Product
和Supplier
对象之间所需的关系。
清单 14-21:更新数据库以包含更改的关系
dotnet ef database update --context EFDatabaseContext
一旦 Entity Framework Core 应用了迁移,使用dotnet run
启动应用程序,并使用浏览器请求 http://localhost:5000 URL。在应用程序启动期间,数据库将植入满足应用于数据库的约束的数据,以便所有Product
对象都与Supplier
相关,如图 14-10 所示。
清单 14-21 所创建的迁移中,外键属性导致两个重要的变化。第一个是防止null
值存入SupplierId
列,它将关系从可选的变为必要的。第二个变化使数据库的工作方式发生了惊人的变化,并造成了许多混乱。
当在定义外键属性之前将导航属性添加到清单 14-3 中的Product
类时,创建的迁移配置如下所示的外键关系:
...
migrationBuilder.AddForeignKey(
name: "FK_Products_Supplier_SupplierId",
table: "Products",
column: "SupplierId",
principalTable: "Supplier",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
...
onDelete
参数告诉数据库在删除Product
所依赖的 Supplier 表中的行时该如何做,使用的值来自ReferentialAction
枚举。Restrict
值用于可选关系,并配置数据库,以便在 Products 表中有依赖于它的行时不能删除Supplier
。这就是在本章前面删除Supplier
时出现错误的原因。
创建必要关系时,外键将被重新配置为不同的ReferentialAction
值,如下所示:
...
migrationBuilder.AddForeignKey(
name: "FK_Products_Supplier_SupplierId",
table: "Products",
column: "SupplierId",
principalTable: "Supplier",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
...
当使用Cascade
值时,删除Supplier
将导致级联删除,这意味着依赖于该Supplier
的任何其他Product
对象也将被删除。要查看级联的效果,使用dotnet run
启动应用程序,并导航至 http://localhost:5000,单击“Unsteady Chair”产品的【Delete】按钮。不但此产品被删除,所有其它关联到“Unsteady Chair”供应商的产品都被删除了,如图 14-11 所示。
重要的是理解触发级联删除的是删除Supplier
对象这个行为。您可以从数据库中移除Product
对象,Supplier
将不受影响。删除Supplier
时,级联删除负责从与要删除的 Supplier 表中的行具有外键关系的 Products 表中删除行。
请记住,执行级联删除的是数据库服务器,而不是 Entity Framework Core。迁移所应用的数据库的配置告诉数据库服务器删除行时应该做什么。我在第22章中直接解释了如何控制这种行为。
大多数应用程序包含多个数据关系。创建、更新和删除更复杂的关联数据的过程是相同的,但是需要一种不同的技术来告诉 Entity Framework Core 遵循所有导航属性,以确保您获得所需的数据。
为演示它是如何工作的,我在 Models 文件夹下新建了一个名为 ContactLocation.cs 的类文件,并添加清单 14-22 中所示的代码:
清单 14-22:Models 文件夹下的 ContactLocation.cs 文件的内容
namespace DataApp.Models
{
public class ContactLocation
{
public long Id { get; set; }
public string LocationName { get; set; }
public string Address { get; set; }
}
}
接下来,我在 Models 文件夹中创建了一个名为 ContactDetails.cs 的类文件,并添加了清单 14-23 所示的代码。
清单 14-23:Models 文件夹下的 ContactDetails.cs 文件的内容
namespace DataApp.Models
{
public class ContactDetails
{
public long Id { get; set; }
public string Name { get; set; }
public string Phone { get; set; }
public ContactLocation Location { get; set; }
}
}
ContactDetails
类定义一个名为Location
的导航属性,该属性创建与ContactLocation
对象的关系。为了完成这些添加,我向Supplier
类添加了一个导航属性,如清单14-24所示。
清单 14-24:Models 文件夹下的 Supplier.cs 文件,添加导航属性
namespace DataApp.Models
{
public class Supplier
{
public long Id { get; set; }
public string Name { get; set; }
public string City { get; set; }
public string State { get; set; }
public ContactDetails Contact { get; set; }
}
}
Contact
属性创建了一个与ContactDetails
类的关系。总体结果是类之间的关系链,如图14-12所示。
要创建允许数据库存储新类实例的迁移,请在 DataApp 项目文件夹中运行清单 14-25 所示的命令。
提示:如果您脱离了本章中的命令的顺序,并开始看到
Invalid Column Name
异常,请注释掉Startup
类中的为数据库播种的语句,并运行清单 14-26 和清单 14-25 中的命令。更新数据库后,取消注释Startup
类中的语句并启动应用程序。
清单 14-25:创建迁移
dotnet ef migrations add AdditionalTypes --context EFDatabaseContext
如果您检查 Migrations 文件夹已创建的<timestamp>_AdditionalTypes.cs
文件中的 C# 语句,将看到 Entity Framework Core 遵循了本章前面解释的惯例,创建了存储ContentDetails
和ContactLocation
对象的表。
若要删除并重新创建数据库,以便数据库重新播种,请在 DataApp 项目文件夹中运行清单 14-26 中的命令。
清单 14-26:重建数据库
dotnet ef database drop --force --context EFDatabaseContext
dotnet ef database update --context EFDatabaseContext
为了确保在数据库被播种时有一些数据可以查询,请将清单 14-27 中所示的语句添加到SeedData
类中Products
属性的 get 访问器中。
清单 14-27:Models 文件夹下的 SeedData.cs 文件,扩展种子数据
...
private static Product[] Products
{
get
{
Product[] products = new Product[]
{
new Product { Name = "Kayak", Category = "Watersports",
Price = 275, Color = Colors.Green, InStock = true },
new Product { Name = "Lifejacket", Category = "Watersports",
Price = 48.95m, Color = Colors.Red, InStock = true },
new Product { Name = "Soccer Ball", Category = "Soccer",
Price = 19.50m, Color = Colors.Blue, InStock = true },
new Product { Name = "Corner Flags", Category = "Soccer",
Price = 34.95m, Color = Colors.Green, InStock = true },
new Product { Name = "Stadium", Category = "Soccer",
Price = 79500, Color = Colors.Red, InStock = true },
new Product { Name = "Thinking Cap", Category = "Chess",
Price = 16, Color = Colors.Blue, InStock = true },
new Product { Name = "Unsteady Chair", Category = "Chess",
Price = 29.95m, Color = Colors.Green, InStock = true },
new Product { Name = "Human Chess Board", Category = "Chess",
Price = 75, Color = Colors.Red, InStock = true },
new Product { Name = "Bling-Bling King", Category = "Chess",
Price = 1200, Color = Colors.Blue, InStock = true }
};
ContactLocation hq = new ContactLocation
{
LocationName = "Corporate HQ",
Address = "200 Acme Way"
};
ContactDetails bob = new ContactDetails
{
Name = "Bob Smith",
Phone = "555-107-1234",
Location = hq
};
Supplier acme = new Supplier
{
Name = "Acme Co",
City = "New York",
State = "NY",
Contact = bob
};
Supplier s1 = new Supplier
{
Name = "Surf Dudes",
City = "San Jose",
State = "CA"
};
Supplier s2 = new Supplier
{
Name = "Chess Kings",
City = "Seattle",
State = "WA"
};
products.First().Supplier = s1;
foreach (Product p in products)
{
if (p == products[0])
{
p.Supplier = s1;
}
else if (p.Category == "Chess")
{
p.Supplier = s2;
}
else
{
p.Supplier = acme;
}
}
return products;
}
}
...
这些变化为Bob Smith
在总部创建了一个新的联系方式,并指定他作为Acme Co
供应商的联系人。
ThenInclude
方法用于扩展查询的范围,以遵循由使用Include
方法选择的类型定义的导航属性。清单14-28使用以下方法来告知 Entity Framework Core 以遵循由Supplier
和ContactDetails
类定义的导航属性,以便图 14-12 所示的所有关系都包含在查询中。
清单 14-28:Models 文件夹下的 EFDatabaseRepository.cs 文件,遵循导航属性
...
public Product GetProduct(long id)
{
return context.Products.Include(p => p.Supplier)
.ThenInclude(s => s.Contact).ThenInclude(c => c.Location)
.First(p => p.Id == id);
}
...
ThenInclude
方法的参数是一个 lambda 函数,它对上一次对Include
或ThenInclude
方法的调用所选择的类型进行操作,并选择您想要遵循的导航属性。通过组合Include
方法和ThenInclude
方法,您可以在复杂模型周围导航,以合并查询中所需的所有数据。
要显示额外的关联数据,请将清单 14-29 中所示的元素添加到 Supplier.cshtml 视图中。
清单 14-29:Views/Shared 文件夹下的 Supplier.cshtml 文件,显示关联数据
@model DataApp.Models.Product
<input name="original.Supplier.Id" value="@Model.Supplier?.Id" type="hidden" />
<input name="original.Supplier.Name" value="@Model.Supplier?.Name" type="hidden" />
<input name="original.Supplier.City" value="@Model.Supplier?.City" type="hidden" />
<input name="original.Supplier.State" value="@Model.Supplier?.State" type="hidden" />
<input type="hidden" asp-for="Supplier.Id" />
<div class="form-group">
<label asp-for="Supplier.Name"></label>
<input asp-for="Supplier.Name" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Supplier.City"></label>
<input asp-for="Supplier.City" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Supplier.State"></label>
<input asp-for="Supplier.State" class="form-control" />
</div>
@if (Model.Supplier?.Contact != null)
{
<div class="form-group">
<label asp-for="Supplier.Contact.Name"></label>
<input asp-for="Supplier.Contact.Name" class="form-control" readonly />
</div>
<div class="form-group">
<label asp-for="Supplier.Contact.Phone"></label>
<input asp-for="Supplier.Contact.Phone" class="form-control" readonly />
</div>
<div class="form-group">
<label asp-for="Supplier.Contact.Location.LocationName"></label>
<input asp-for="Supplier.Contact.Location.LocationName"
class="form-control" readonly />
</div>
}
此次添加是只读元素,显示扩展的关联数据值。要测试查询,请使用dotnet run
启动应用程序,导航到 http://localhost:5000,然后单击某个供应商是“Acme Co”的产品的【Edit】按钮。您将看到联系方式,如图14-13所示。
本章,我解释了如何使用导航属性来创建数据之间的关系,以及它们是如何反映在数据库中并被 Entity Framework Core 所消耗的。下一章,我将演示如何使用不同的关系特性。
;